package container

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"cloudea.com/container/bridge"
	"cloudea.com/container/config"
	"cloudea.com/container/domain/image"
	"cloudea.com/container/utils"
	"github.com/vishvananda/netlink"
	"golang.org/x/sys/unix"
)

/**
领域模型相关
@author 学者科技
@date 2022/5/16
*/

type ContainerStatus string

const (
	CONTAINER_RUNNING ContainerStatus = "RUNNUNG"
	CONTAINER_STOPPED ContainerStatus = "STOPPED"
)

/*
*
容器的模型
*/

type Container struct {
	Name        string          //容器的名称（同时也是namespace的名称）
	Image       image.Image     // 容器引用的镜像
	Status      ContainerStatus // 容器的状态
	CreatedTime time.Time       // 创建的时间
	ThreadId    int             // 主进程的id， 0表示没有
	Command     []string        // 容器覆盖镜像的命令
	Ip          string          // 容器的ip地址
	Hash        string          // 容器的哈希值
	// Cpu  int    // 最大cpu核数
	// Mem  int    // 最大内存M数
}

/*
*
运行这个容器
*/
func (container *Container) Run(isDaemon bool, onRun func()) error {
	if container.Status == CONTAINER_RUNNING {
		return errors.New("不能重复运行容器")
	}
	//rootFsLocation := REPOSITORY.GetFs(*container)
	// 提前决定有没有有命令， 用户输入的应该覆盖镜像的command
	command := container.GetFullCommand()
	if len(command) == 0 {
		return errors.New("容启没有启动命令")
	}

	// 挂载文件系统
	// 注意：mount命令是允许多次挂载的。即一个挂载点即使已经挂载了，还是可以挂载上去
	containerBase := config.GetContainerBase(container.Name)
	containerMerged := containerBase + "/merged"
	if !utils.IsMounted(containerMerged) {
		lowerDirs := ""
		lowerSet := make(map[string]bool)
		for index, layer := range container.Image.GetLayers() {
			if _, ok := lowerSet[layer]; ok {
				continue
			}
			if index != 0 {
				lowerDirs += ":"
			}
			lowerSet[layer] = true                       // 使用map去重，防止overlayfs2挂不上
			lowerDirs += config.GetImageLayerBase(layer) // 最近的放在最左边，刚好跟配置文件顺序一致
		}

		process, err := os.StartProcess(
			"/usr/bin/mount",
			[]string{
				"/usr/bin/mount",
				"-t",
				"overlay",
				"overlay",
				"-o",
				fmt.Sprintf("lowerdir=%s,upperdir=diff,workdir=work", lowerDirs),
				"merged",
			},
			&os.ProcAttr{
				Dir: containerBase,
				Env: os.Environ(),
				Sys: &syscall.SysProcAttr{},
				Files: []*os.File{
					os.Stdin,
					os.Stdout,
					os.Stderr,
				},
			})

		if err != nil {
			panic(err)
		}

		state, err := process.Wait()
		if err != nil {
			panic(err)
		}

		if state.ExitCode() != 0 {
			panic("挂载联合文件系统出错")
		}

		defer func() {
			err := recover()
			if err != nil {
				// 如果在创建过程中出现一些错误，那么则删除挂载点
				process, _ := os.StartProcess(
					"/usr/bin/umount",
					[]string{
						"/usr/bin/umount",
						"-l",
						"merged",
					},
					&os.ProcAttr{
						Dir: containerBase,
						Env: os.Environ(),
						Sys: &syscall.SysProcAttr{},
						Files: []*os.File{
							os.Stdin,
							os.Stdout,
							os.Stderr,
						},
					})
				process.Wait()
				panic(err)
			}
		}()
	}

	// 构建具有新名字空间的子进程
	c := exec.Command("/proc/self/exe", "sub", container.Name)
	c.Env = nil
	c.Dir = ""
	if !isDaemon {
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
	}
	/**
	CLONE_NEWCGROUP                             = 0x2000000
	CLONE_NEWIPC                                = 0x8000000   // 进程间通信
	CLONE_NEWNET                                = 0x40000000  // 网络名字空间
	CLONE_NEWNS                                 = 0x20000     // 挂载点(新起的容器，默认没有任何挂载，需手动挂载)
	CLONE_NEWPID                                = 0x20000000  // 进程id
	CLONE_NEWTIME                               = 0x80
	CLONE_NEWUSER                               = 0x10000000  // 新用户， 没啥用
	CLONE_NEWUTS                                = 0x4000000   // 可以设置独立于宿主机的域名
	*/

	c.SysProcAttr = &syscall.SysProcAttr{
		//Chroot: rootFsLocation,
		Cloneflags: unix.CLONE_NEWPID |
			unix.CLONE_NEWNS |
			unix.CLONE_NEWUTS |
			//unix.CLONE_NEWUSER |
			unix.CLONE_NEWNET |
			unix.CLONE_NEWIPC,
		// UidMappings: []syscall.SysProcIDMap{
		// 	{
		// 		ContainerID: 0,
		// 		HostID:      syscall.Getuid(),
		// 		Size:        1,
		// 	},
		// },
		// GidMappings: []syscall.SysProcIDMap{
		// 	{
		// 		ContainerID: 0,
		// 		HostID:      syscall.Getgid(),
		// 		Size:        1,
		// 	},
		// },
	}
	err := c.Start()
	if err != nil {
		return err
	}

	// 更新容器自已的状态
	container.Status = CONTAINER_RUNNING
	container.ThreadId = c.Process.Pid

	if onRun != nil {
		onRun()
	}

	// 设置网络
	container.createLink()

	// 决定是否立即退出
	if !isDaemon {
		return c.Wait()
	}

	return nil
}

/** 在新的名字空间中跑 */
func (container *Container) Run2() error {
	command := container.GetFullCommand()
	if len(command) == 0 {
		return errors.New("容启没有启动命令")
	}
	// 切换根目录
	utils.Assert(unix.Chroot(REPOSITORY.GetFs(*container)))

	// 删除临时目录里的文件
	utils.FileClear("/tmp")
	utils.FileClear("/run")

	// 挂载必要的文件系统
	os.MkdirAll("/dev/pts", os.ModePerm)
	utils.Assert(unix.Mount("proc", "/proc", "proc", 0, ""))
	unix.Mount("sysfs", "/sys", "sysfs", 0, "")
	utils.Assert(unix.Mount("devpts", "/dev/pts", "devpts", 0, ""))
	//unix.Mount("tmpfs", "/dev", "tmpfs", 0, "")

	// 更改hostname
	utils.Command("hostname", container.Hash)

	// 修复bug
	// 1. DNS问题：复制DNS文件
	// 2. 终端无法识别问题
	utils.Assert(utils.FileWriteString("/etc/resolv.conf", "nameserver 8.8.8.8\n"))

	utils.Assert(bridge.RemoveContainer0()) // 手工删除containe0, 不知道为啥container0跑到了容器的网络名字空间中。如果存在的话，网络无法ping通
	container.setUpLocalHost()
	container.setUpLink()

	// 构建具有新名字空间的子进程
	c := exec.Command(command[0], command[1:]...)
	c.Env = append(container.Image.GetEnviron(), "TERM=xterm")
	c.Dir = utils.StringEmptyDefault(container.Image.GetWorkDir(), "/")
	c.Stdin = os.Stdin
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr

	defer func() {
		recover()
	}()

	utils.Assert(c.Start())
	utils.Assert(c.Wait())

	container.setDownLink()

	// 取消挂载
	unix.Unmount("/proc", 0)
	unix.Unmount("/sys", 0)
	unix.Unmount("/dev/pts", 0)

	return nil
}

/** 获取实际运行的指令 */
func (container *Container) GetFullCommand() []string {
	// 组装启动命令
	command := container.Image.GetEntryPoint()
	if len(container.Command) != 0 {
		command = append(command, container.Command...)
	} else {
		command = append(command, container.Image.GetCommand()...)
	}
	return command
}

/*
*
在容器上运行一条命令
@param cmd string 要执行的命令
*/
func (container *Container) Exec(isDaemon bool, command []string) error {
	if container.Status != CONTAINER_RUNNING {
		return errors.New("该容器没有在运行")
	}

	// 打开文件
	namespaces := REPOSITORY.GetNs(*container)
	var namespaceFds = make(map[ContainerNamespace]*os.File)
	for _, ns := range namespaces {
		file, err := os.OpenFile(ns.Filename, os.O_RDONLY, 0)
		if err != nil {
			return err
		}
		namespaceFds[ns] = file
	}

	// chroot
	utils.Assert(unix.Chroot(REPOSITORY.GetFs(*container)))

	// 设置namespace
	for _, ns := range namespaces {
		file := namespaceFds[ns]
		err := unix.Setns(int(file.Fd()), ns.Type)
		if err != nil {
			//fmt.Println(ns.Filename)
			//return err
		}
	}

	// 执行命令
	c := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(command, " "))...)
	c.Env = append(container.Image.GetEnviron(), "TERM=xterm")
	c.Dir = utils.StringEmptyDefault(container.Image.GetWorkDir(), "/")
	if !isDaemon {
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
	}
	err := c.Start()
	if err != nil {
		return err
	}

	// 等待结束
	if !isDaemon {
		return c.Wait()
	}

	return nil
}

/** 设置本地回环地址 */
func (container *Container) setUpLocalHost() {
	link, err := netlink.LinkByName("lo")
	if err == nil && link != nil {
		loAddr, _ := netlink.ParseAddr("127.0.0.1/8")
		netlink.AddrAdd(link, loAddr)
		netlink.LinkSetUp(link)
	}
}

/** 创建veth并放到正确的名字空间中 */
func (container *Container) createLink() {
	bridge, _ := netlink.LinkByName(config.CONTAINER0)
	veth0 := "veth0_" + container.Hash[:9]
	veth1 := "veth1_" + container.Hash[:9]
	// 创建veth
	attrs := netlink.NewLinkAttrs()
	attrs.Name = veth0
	veth := &netlink.Veth{
		LinkAttrs:        attrs,
		PeerName:         veth1,
		PeerHardwareAddr: utils.CreateMACAddress(),
	}
	if err := netlink.LinkAdd(veth); err != nil {
		fmt.Println(err.Error())
	}

	// 添加到bridge
	if err := netlink.LinkSetMaster(veth, bridge); err != nil {
		fmt.Println(err.Error())
	}

	// 设置peer的NS
	// 设置NS后设置IP无效
	veth1Device, _ := netlink.LinkByName(veth1)
	veth1NetFilename := fmt.Sprintf("/proc/%d/ns/net", container.ThreadId)
	veth1NetFile, _ := os.Open(veth1NetFilename)
	netlink.LinkSetNsFd(veth1Device, int(veth1NetFile.Fd()))

	if err := netlink.LinkSetUp(veth); err != nil {
		fmt.Println(err.Error())
	}
}

/** 设置veth */
func (container *Container) setUpLink() {
	veth1 := "veth1_" + container.Hash[:9]
	bridgeNet, mask := utils.IPCIDRParse(config.CONTAINER0_CIDR)
	containerIP, _ := utils.IPAdrrToInt(container.Ip)
	veth1Device, _ := netlink.LinkByName(veth1)
	// 设置IP
	veth1Addr, _ := netlink.ParseAddr(utils.IPCIDREncode(containerIP, mask))
	netlink.AddrAdd(veth1Device, veth1Addr)
	bridgeIP := utils.IntToIPAddr(bridgeNet + 1)

	// 更改接口名字(在UP前设置)
	utils.Assert(netlink.LinkSetName(veth1Device, "eth0"))

	// UP
	if err := netlink.LinkSetUp(veth1Device); err != nil {
		fmt.Println(err.Error())
	}
	// 添加网关
	// 必需在UP后，才能添加网关
	utils.Assert(netlink.RouteAdd(&netlink.Route{
		LinkIndex: veth1Device.Attrs().Index,
		//ILinkIndex int
		Scope: netlink.SCOPE_UNIVERSE,
		Gw:    net.ParseIP(bridgeIP),
		Dst:   nil,
	}))

}

/** 删除veth */
func (container *Container) setDownLink() {
	// veth 不需要显式的删除，当容器退出时会自动消失
	//veth0 := "veth0_" + container.Hash[:9]
	veth1 := "veth1_" + container.Hash[:9]
	veth, _ := netlink.LinkByName(veth1)
	if veth != nil {
		netlink.LinkSetNoMaster(veth)
		netlink.LinkSetDown(veth)
		netlink.LinkDel(veth)
	}
}

/** 产生一个哈希值 */
func GenHashCode() string {
	hash := make([]byte, 5)
	rand.Read(hash)
	return hex.EncodeToString(hash)
}

/** 限制CPU和内存资源 */
func (container *Container) LimitResource(cpuQuota string, memQuota string) {
	path := config.GetContainerCgroup(container.Name)
	processFile := path + "/" + "cgroup.procs"
	memoryFile := path + "/" + "memory.max"
	cpuFile := path + "/" + "cpu.max"
	utils.Assert(utils.Shell(fmt.Sprintf("echo %d > %s", container.ThreadId, processFile)))
	utils.Assert(utils.Shell(fmt.Sprintf("echo %s > %s", memQuota, memoryFile)))
	utils.Assert(utils.Shell(fmt.Sprintf("echo '%s %d'  > %s", cpuQuota, config.CPU_PERIOD, cpuFile)))
}
